/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.jenetics.example.image;

import static java.lang.String.format;
import static javax.swing.SwingUtilities.invokeLater;
import static io.jenetics.example.image.EvolvingImagesCmd.writeImage;

import java.awt.Dimension;
import java.awt.Toolkit;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.Serial;
import java.text.NumberFormat;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.prefs.BackingStoreException;
import java.util.prefs.Preferences;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import javax.imageio.ImageIO;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.filechooser.FileNameExtensionFilter;

import io.jenetics.Genotype;
import io.jenetics.engine.EvolutionResult;

/**
 * This example shows a more advanced use of a genetic algorithm: approximate a
 * raster image with ~100 semi-transparent polygons of length 6.
 * <p>
 * The fitness function is quite simple yet expensive to compute:
 * <ul>
 * <li>draw the polygons of a chromosome to an image
 * <li>compare each pixel with the corresponding reference image
 * </ul>
 * <p>
 * To improve the speed of the calculation, we calculate the fitness not on the
 * original image size, but rather on a scaled down version, which is sufficient
 * to demonstrate the power of such a genetic algorithm.
 *
 * @see <a href="http://www.nihilogic.dk/labs/evolving-images/">
 *      Evolving Images with JavaScript and canvas (Nihilogic)</a>
 */
public class EvolvingImages extends JFrame {

	@Serial
	private static final long serialVersionUID = 1L;

	// Additional Swing components.
	private final NumberFormat _fitnessFormat = NumberFormat.getNumberInstance();
	private final ImagePanel _imagePanel;
	private final PolygonPanel _polygonPanel;

	private volatile EvolvingImagesWorker _worker;

	/**
	 * Creates new form ImageEvolution
	 */
	public EvolvingImages() {
		_imagePanel = new ImagePanel();
		_polygonPanel = new PolygonPanel();

		initComponents();
		init();
	}

	private void init() {
		setIconImage(
			Toolkit.getDefaultToolkit().getImage(getClass().getResource(
				"/io/jenetics/example/image/monalisa.png"))
		);

		origImagePanel.add(_imagePanel);
		polygonImagePanel.add(_polygonPanel);
		engineParamPanel.setEngineParam(engineParam());

		_fitnessFormat.setMaximumIntegerDigits(1);
		_fitnessFormat.setMinimumIntegerDigits(1);
		_fitnessFormat.setMinimumFractionDigits(5);
		_fitnessFormat.setMaximumFractionDigits(5);

		imageSplitPane.setDividerLocation(0.5);

		startButton.setEnabled(true);
		stopButton.setEnabled(false);
		pauseButton.setEnabled(false);
		saveButton.setEnabled(false);

		try (InputStream in = getClass()
			.getClassLoader()
			.getResourceAsStream("io/jenetics/example/image/monalisa.png"))
		{
			update(ImageIO.read(in));
		} catch (IOException e) {
			throw new AssertionError(e);
		}
	}

	private void update(final BufferedImage image) {
		_imagePanel.setImage(image);
		_polygonPanel.setDimension(image.getWidth(), image.getHeight());
	}

	private EngineParam getEngineParam() {
		final EngineParam param = engineParamPanel.getEngineParam();
		engineParam(param);
		return param;
	}

	private BufferedImage getImage() {
		return _imagePanel.getImage();
	}

	/**
	 * This method is called from within the constructor to initialize the form.
	 * WARNING: Do NOT modify this code. The content of this method is always
	 * regenerated by the Form Editor.
	 */
	@SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">//GEN-BEGIN:initComponents
    private void initComponents() {
        java.awt.GridBagConstraints gridBagConstraints;

        imagePanel = new javax.swing.JPanel();
        imageSplitPane = new javax.swing.JSplitPane();
        origImagePanel = new javax.swing.JPanel();
        polygonImagePanel = new javax.swing.JPanel();
        buttonPanel = new javax.swing.JPanel();
        startButton = new javax.swing.JButton();
        stopButton = new javax.swing.JButton();
        openButton = new javax.swing.JButton();
        pauseButton = new javax.swing.JButton();
        saveButton = new javax.swing.JButton();
        resultPanel = new javax.swing.JPanel();
        bestEvolutionResultPanel = new io.jenetics.example.image.EvolutionResultPanel();
        currentevolutionResultPanel = new io.jenetics.example.image.EvolutionResultPanel();
        engineParamPanel = new io.jenetics.example.image.EngineParamPanel();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setTitle("Evolving images");

        imagePanel.setBackground(new java.awt.Color(153, 153, 153));
        imagePanel.setLayout(new java.awt.GridLayout(1, 1));

        imageSplitPane.setDividerLocation(300);

        origImagePanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Source image"));
        origImagePanel.setName(""); // NOI18N
        origImagePanel.addComponentListener(new java.awt.event.ComponentAdapter() {
            public void componentResized(java.awt.event.ComponentEvent evt) {
                origImagePanelComponentResized(evt);
            }
        });
        origImagePanel.setLayout(new java.awt.BorderLayout());
        imageSplitPane.setLeftComponent(origImagePanel);

        polygonImagePanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Polygon image"));
        polygonImagePanel.setLayout(new java.awt.GridLayout(1, 1));
        imageSplitPane.setRightComponent(polygonImagePanel);

        imagePanel.add(imageSplitPane);

        startButton.setText("Start");
        startButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                startButtonActionPerformed(evt);
            }
        });

        stopButton.setText("Stop");
        stopButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                stopButtonActionPerformed(evt);
            }
        });

        openButton.setText("Open");
        openButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                openButtonActionPerformed(evt);
            }
        });

        pauseButton.setText("Pause");
        pauseButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                pauseButtonActionPerformed(evt);
            }
        });

        saveButton.setText("Save");
        saveButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(java.awt.event.ActionEvent evt) {
                saveButtonActionPerformed(evt);
            }
        });

        javax.swing.GroupLayout buttonPanelLayout = new javax.swing.GroupLayout(buttonPanel);
        buttonPanel.setLayout(buttonPanelLayout);
        buttonPanelLayout.setHorizontalGroup(
            buttonPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(buttonPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addGroup(buttonPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(startButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(stopButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(openButton, javax.swing.GroupLayout.DEFAULT_SIZE, 119, Short.MAX_VALUE)
                    .addComponent(pauseButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(saveButton, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addContainerGap())
        );
        buttonPanelLayout.setVerticalGroup(
            buttonPanelLayout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(buttonPanelLayout.createSequentialGroup()
                .addContainerGap()
                .addComponent(startButton)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(stopButton)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(pauseButton)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, 196, Short.MAX_VALUE)
                .addComponent(openButton)
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(saveButton)
                .addContainerGap())
        );

        resultPanel.setLayout(new java.awt.GridBagLayout());

        bestEvolutionResultPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Best"));
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 1;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
        gridBagConstraints.weightx = 1.0;
        gridBagConstraints.insets = new java.awt.Insets(2, 2, 2, 2);
        resultPanel.add(bestEvolutionResultPanel, gridBagConstraints);

        currentevolutionResultPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Current"));
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 0;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST;
        gridBagConstraints.weightx = 1.0;
        resultPanel.add(currentevolutionResultPanel, gridBagConstraints);

        engineParamPanel.setBorder(javax.swing.BorderFactory.createTitledBorder("Engine parameter"));
        gridBagConstraints = new java.awt.GridBagConstraints();
        gridBagConstraints.gridx = 0;
        gridBagConstraints.gridy = 1;
        gridBagConstraints.gridwidth = 2;
        gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL;
        resultPanel.add(engineParamPanel, gridBagConstraints);

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(resultPanel, javax.swing.GroupLayout.DEFAULT_SIZE, 788, Short.MAX_VALUE)
                    .addGroup(layout.createSequentialGroup()
                        .addComponent(imagePanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                        .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED)
                        .addComponent(buttonPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)))
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup()
                .addContainerGap()
                .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                    .addComponent(buttonPanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE)
                    .addComponent(imagePanel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
                .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED)
                .addComponent(resultPanel, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE)
                .addContainerGap())
        );

        pack();
    }// </editor-fold>//GEN-END:initComponents

	private void startButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_startButtonActionPerformed
		_worker = EvolvingImagesWorker.of(getEngineParam(), getImage());
		_worker.start(this::onNewResult);

		// Enable/Disable UI controls.
		startButton.setEnabled(false);
		stopButton.setEnabled(true);
		pauseButton.setEnabled(true);
		openButton.setEnabled(false);
		saveButton.setEnabled(true);
		engineParamPanel.setEnabled(false);
	}//GEN-LAST:event_startButtonActionPerformed

	private void stopButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_stopButtonActionPerformed
		_worker.stop();

		startButton.setEnabled(true);
		stopButton.setEnabled(false);
		pauseButton.setEnabled(false);
		pauseButton.setText("Pause");
		openButton.setEnabled(true);
		engineParamPanel.setEnabled(true);
	}//GEN-LAST:event_stopButtonActionPerformed

	private void openButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_openButtonActionPerformed
		final File dir = lastOpenDirectory();
		final JFileChooser chooser = dir != null
			? new JFileChooser(dir)
			: new JFileChooser();
		chooser.setDialogTitle("Choose Image");
		chooser.setDialogType(JFileChooser.OPEN_DIALOG);
		chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
		chooser.setMultiSelectionEnabled(false);
		chooser.addChoosableFileFilter(new FileNameExtensionFilter(
			format(
				"Images (%s)",
				Stream.of(ImageIO.getReaderFileSuffixes())
					.map(s -> format(" *.%s", s))
					.collect(Collectors.joining(","))
			),
			ImageIO.getReaderFileSuffixes()
		));

		final int returnVal = chooser.showOpenDialog(this);
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			final File imageFile = chooser.getSelectedFile();
			try {
				update(ImageIO.read(imageFile));
				if (imageFile.getParentFile() != null) {
					lastOpenDirectory(imageFile.getParentFile());
				}
			} catch (IOException e) {
				JOptionPane.showMessageDialog(
					rootPane,
					format("Error while loading image '%s'.", imageFile),
					e.toString(),
					JOptionPane.ERROR_MESSAGE
				);
			}
		}
	}//GEN-LAST:event_openButtonActionPerformed

    private void origImagePanelComponentResized(java.awt.event.ComponentEvent evt) {//GEN-FIRST:event_origImagePanelComponentResized
        origImagePanel.repaint();
		polygonImagePanel.repaint();
    }//GEN-LAST:event_origImagePanelComponentResized

    private void pauseButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_pauseButtonActionPerformed
        switch (pauseButton.getText()) {
			case "Pause":
				_worker.pause();
				pauseButton.setText("Resume");
				break;
			case "Resume":
				_worker.resume();
				pauseButton.setText("Pause");
				break;
			default:
		}
    }//GEN-LAST:event_pauseButtonActionPerformed

    private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed
		final File dir = lastSaveDirectory();
		final JFileChooser chooser = dir != null
			? new JFileChooser(dir)
			: new JFileChooser();
		chooser.setDialogTitle("Save Polygon Image");
		chooser.setDialogType(JFileChooser.SAVE_DIALOG);
		chooser.setFileSelectionMode(JFileChooser.FILES_ONLY);
		chooser.setMultiSelectionEnabled(false);
		chooser.addChoosableFileFilter(new FileNameExtensionFilter(
			"Images (*.png)",
			ImageIO.getReaderFileSuffixes()
		));

		final int returnVal = chooser.showSaveDialog(this);
		if (returnVal == JFileChooser.APPROVE_OPTION) {
			final String name = chooser.getSelectedFile().getAbsolutePath();
			final File imageFile = name.toLowerCase().endsWith(".png")
				? new File(name)
				: new File(name + ".png");

			try {
				final Dimension dim = _polygonPanel.getDimension();
				final PolygonChromosome ch = _polygonPanel.getChromosome();
				writeImage(imageFile, ch, dim.width, dim.height);

				if (imageFile.getParentFile() != null) {
					lastSaveDirectory(imageFile.getParentFile());
				}
			} catch (Exception e) {
				JOptionPane.showMessageDialog(
					rootPane,
					format("Error while saving image '%s'.", imageFile),
					e.toString(),
					JOptionPane.ERROR_MESSAGE
				);
			}
		}
    }//GEN-LAST:event_saveButtonActionPerformed

	private void onNewResult(
		final EvolutionResult<PolygonGene, Double> current,
		final EvolutionResult<PolygonGene, Double> best
	) {
		invokeLater(() -> {
			final Genotype<PolygonGene> gt = best
				.bestPhenotype()
				.genotype();

			bestEvolutionResultPanel.update(best);
			currentevolutionResultPanel.update(current);
			_polygonPanel.setChromosome((PolygonChromosome)gt.chromosome());
			_polygonPanel.repaint();
		});
	}

	/* *************************************************************************
	 * Application preferences.
	 **************************************************************************/

	private static final String ENGINE_PARAM_NODE = "engine_param";
	private static final String LAST_OPEN_DIRECTORY_PREF = "last_open_directory";
	private static final String LAST_SAVE_DIRECTORY_PREF = "last_save_directory";

	private EngineParam engineParam() {
		return EngineParam.load(appPref().node(ENGINE_PARAM_NODE));
	}

	private void engineParam(final EngineParam param) {
		param.store(appPref().node(ENGINE_PARAM_NODE));
	}

	private File lastOpenDirectory() {
		final String dirName = appPref().get(LAST_OPEN_DIRECTORY_PREF, null);
		return dirName != null ? new File(dirName) : null;
	}

	private void lastOpenDirectory(final File dir) {
		appPref().put(LAST_OPEN_DIRECTORY_PREF, dir.getAbsolutePath());
	}

	private File lastSaveDirectory() {
		final String dirName = appPref().get(LAST_SAVE_DIRECTORY_PREF, null);
		return dirName != null ? new File(dirName) : null;
	}

	private void lastSaveDirectory(final File dir) {
		appPref().put(LAST_SAVE_DIRECTORY_PREF, dir.getAbsolutePath());
	}

	private static Preferences appPref() {
		return Preferences.userRoot().node("io/jenetics/example/image");
	}

	private static void prefFlush() {
		try {
			appPref().flush();
		} catch (BackingStoreException ex) {
			Logger.getLogger(EvolvingImages.class.getName())
				.log(Level.SEVERE, null, ex);
		}
	}

	/**
	 * @param args the command line arguments
	 */
	public static void main(final String[] args) {
		// Start command line version if the right parameters are given.
		if (new EvolvingImagesCmd(args).run()) return;

		/* Set the Nimbus look and feel */
		//<editor-fold defaultstate="collapsed" desc=" Look and feel setting code (optional) ">
        /* If Nimbus (introduced in Java SE 6) is not available, stay with the default look and feel.
         * For details see http://download.oracle.com/javase/tutorial/uiswing/lookandfeel/plaf.html
         */
		try {
			for (javax.swing.UIManager.LookAndFeelInfo info : javax.swing.UIManager.getInstalledLookAndFeels()) {
				if ("Nimbus".equals(info.getName())) {
					javax.swing.UIManager.setLookAndFeel(info.getClassName());
					break;
				}
			}
		} catch (ClassNotFoundException |
				InstantiationException |
				IllegalAccessException |
				javax.swing.UnsupportedLookAndFeelException ex)
		{
			java.util.logging.Logger
					.getLogger(EvolvingImages.class.getName())
					.log(java.util.logging.Level.SEVERE, null, ex);
		}
		//</editor-fold>
		//</editor-fold>
		//</editor-fold>
		//</editor-fold>

		//</editor-fold>
		//</editor-fold>
		//</editor-fold>
		//</editor-fold>

        /* Create and display the form */
		java.awt.EventQueue.invokeLater(() -> {
			new EvolvingImages().setVisible(true);
			prefFlush();
		});
	}

    // Variables declaration - do not modify//GEN-BEGIN:variables
    private io.jenetics.example.image.EvolutionResultPanel bestEvolutionResultPanel;
    private javax.swing.JPanel buttonPanel;
    private io.jenetics.example.image.EvolutionResultPanel currentevolutionResultPanel;
    private io.jenetics.example.image.EngineParamPanel engineParamPanel;
    private javax.swing.JPanel imagePanel;
    private javax.swing.JSplitPane imageSplitPane;
    private javax.swing.JButton openButton;
    private javax.swing.JPanel origImagePanel;
    private javax.swing.JButton pauseButton;
    private javax.swing.JPanel polygonImagePanel;
    private javax.swing.JPanel resultPanel;
    private javax.swing.JButton saveButton;
    private javax.swing.JButton startButton;
    private javax.swing.JButton stopButton;
    // End of variables declaration//GEN-END:variables

}
